/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.networkmanager.admin.impl;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Authenticator;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.UnsupportedAddressTypeException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEDiagnosticsEvidenceGenerator;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AddressUtils;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.platform.PlatformManager;
import org.gudy.azureus2.platform.PlatformManagerCapabilities;
import org.gudy.azureus2.platform.PlatformManagerFactory;
import org.gudy.azureus2.platform.PlatformManagerPingCallback;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.platform.PlatformManagerException;
import org.gudy.azureus2.plugins.ui.UIManager;
import org.gudy.azureus2.plugins.ui.UIManagerEvent;
import org.gudy.azureus2.plugins.utils.StaticUtilities;
import org.gudy.azureus2.plugins.utils.Utilities;
import org.gudy.azureus2.pluginsimpl.local.PluginInitializer;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.AzureusCoreRunningListener;
import com.aelitis.azureus.core.instancemanager.AZInstance;
import com.aelitis.azureus.core.instancemanager.AZInstanceManager;
import com.aelitis.azureus.core.instancemanager.AZInstanceManagerListener;
import com.aelitis.azureus.core.instancemanager.AZInstanceTracked;
import com.aelitis.azureus.core.networkmanager.NetworkConnectionBase;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.networkmanager.Transport;
import com.aelitis.azureus.core.networkmanager.TransportBase;
import com.aelitis.azureus.core.networkmanager.TransportStartpoint;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminASN;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminASNListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminException;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminHTTPProxy;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminNATDevice;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminNetworkInterface;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminNetworkInterfaceAddress;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminNode;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminPropertyChangeListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminProtocol;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminRouteListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminRoutesListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSocksProxy;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduler;
import com.aelitis.azureus.core.networkmanager.impl.http.HTTPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager;
import com.aelitis.azureus.core.proxy.AEProxySelectorFactory;
import com.aelitis.azureus.core.proxy.socks.AESocksProxy;
import com.aelitis.azureus.core.proxy.socks.AESocksProxyFactory;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.NetUtils;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;
import com.aelitis.azureus.plugins.upnp.UPnPPluginService;

public class NetworkAdminImpl extends NetworkAdmin implements AEDiagnosticsEvidenceGenerator {
    private static final LogIDs LOGID = LogIDs.NWMAN;

    private static final boolean FULL_INTF_PROBE = false;

    private static InetAddress anyLocalAddress;
    private static InetAddress anyLocalAddressIPv4;
    private static InetAddress anyLocalAddressIPv6;
    private static InetAddress localhostV4;
    private static InetAddress localhostV6;

    static {
        try {
            anyLocalAddressIPv4 = InetAddress.getByAddress(new byte[] { 0, 0, 0, 0 });
            anyLocalAddressIPv6 = InetAddress.getByAddress(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
            anyLocalAddress = new InetSocketAddress(0).getAddress();
            localhostV4 = InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 });
            localhostV6 = InetAddress.getByAddress(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 });
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    private static final int INTERFACE_CHECK_MILLIS = 15 * 1000;
    private static final int ROUTE_CHECK_MILLIS = 60 * 1000;
    private static final int ROUTE_CHECK_TICKS = ROUTE_CHECK_MILLIS / INTERFACE_CHECK_MILLIS;

    private Set<NetworkInterface> old_network_interfaces;
    private Map<String, AddressHistoryRecord> address_history = new HashMap<String, AddressHistoryRecord>();
    private long address_history_update_time;

    private InetAddress[] currentBindIPs = new InetAddress[] { null };
    private boolean forceBind = false;
    private boolean supportsIPv6withNIO = true;
    private boolean supportsIPv6 = true;
    private boolean supportsIPv4 = true;

    private boolean IPv6_enabled;

    {
        COConfigurationManager.addAndFireParameterListener("IPV6 Enable Support", new ParameterListener() {
            public void parameterChanged(String parameterName) {
                setIPv6Enabled(COConfigurationManager.getBooleanParameter("IPV6 Enable Support"));
            }
        });

        COConfigurationManager.addResetToDefaultsListener(new COConfigurationManager.ResetToDefaultsListener() {
            public void reset() {
                clearMaybeVPNs();
            }
        });
    }

    private int roundRobinCounterV4 = 0;
    private int roundRobinCounterV6 = 0;

    private boolean logged_bind_force_issue;

    private CopyOnWriteList listeners = new CopyOnWriteList();

    private NetworkAdminRouteListener trace_route_listener = new NetworkAdminRouteListener() {
        private int node_count = 0;

        public boolean foundNode(NetworkAdminNode node, int distance, int rtt) {
            node_count++;

            return (true);
        }

        public boolean timeout(int distance) {
            if (distance == 3 && node_count == 0) {

                return (false);
            }

            return (true);
        }
    };

    private static final int ASN_MIN_CHECK = 30 * 60 * 1000;

    private long last_asn_lookup_time;

    private List asn_ips_checked = new ArrayList(0);

    private List as_history = new ArrayList();

    private AsyncDispatcher async_asn_dispacher = new AsyncDispatcher();
    private static final int MAX_ASYNC_ASN_LOOKUPS = 1024;

    private Map<InetAddress, NetworkAdminASN> async_asn_history = new LinkedHashMap<InetAddress, NetworkAdminASN>(256, 0.75f, true) {
        protected boolean removeEldestEntry(Map.Entry<InetAddress, NetworkAdminASN> eldest) {
            return size() > 256;
        }
    };

    private boolean initialised;

    public NetworkAdminImpl() {
        COConfigurationManager.addParameterListener(new String[] { "Bind IP", "Enforce Bind IP" }, new ParameterListener() {
            public void parameterChanged(String parameterName) {
                checkDefaultBindAddress(false);
            }
        });

        SimpleTimer.addPeriodicEvent("NetworkAdmin:checker", INTERFACE_CHECK_MILLIS, new TimerEventPerformer() {
            private int tick_count;

            public void perform(TimerEvent event) {
                tick_count++;

                boolean changed = checkNetworkInterfaces(false, false);

                if (changed || tick_count % ROUTE_CHECK_TICKS == 0) {

                    checkConnectionRoutes();
                }
            }
        });

        // populate initial values

        checkNetworkInterfaces(true, true);

        checkDefaultBindAddress(true);

        AEDiagnostics.addEvidenceGenerator(this);

        checkDNSSPI();

        AzureusCoreFactory.addCoreRunningListener(new AzureusCoreRunningListener() {
            public void azureusCoreRunning(AzureusCore core) {
                try {
                    Class.forName("com.aelitis.azureus.core.networkmanager.admin.impl.swt.NetworkAdminSWTImpl").getConstructor(
                            new Class[] { AzureusCore.class, NetworkAdminImpl.class }).newInstance(new Object[] { core, NetworkAdminImpl.this });

                } catch (Throwable e) {
                }
            }
        });

        initialised = true;
    }

    private void checkDNSSPI() {
        String error_str = null;

        try {
            InetAddress ia = InetAddress.getByName("dns.test.client.vuze.com");

            if (ia.isLoopbackAddress()) {

                // looks good!

            } else {

                error_str = "Loopback address expected, got " + ia;
            }
        } catch (UnknownHostException e) {

            error_str = "DNS SPI not loaded";

        } catch (Throwable e) {

            error_str = "Test lookup failed: " + Debug.getNestedExceptionMessage(e);
        }

        if (error_str != null) {

            Logger.log(new LogAlert(true, LogAlert.AT_WARNING, MessageText.getString("network.admin.dns.spi.fail", error_str)));
        }
    }

    protected void setIPv6Enabled(boolean enabled) {
        IPv6_enabled = enabled;

        supportsIPv6withNIO = enabled;
        supportsIPv6 = enabled;

        if (initialised) {

            checkNetworkInterfaces(false, true);

            checkDefaultBindAddress(false);
        }
    }

    public boolean isIPV6Enabled() {
        return (IPv6_enabled);
    }

    private List<NetworkInterface> last_getni_result;
    private Object getni_lock = new Object();

    protected boolean checkNetworkInterfaces(boolean first_time, boolean force) {
        boolean changed = false;

        try {
            List<NetworkInterface> x = NetUtils.getNetworkInterfaces();

            boolean fire_stuff = false;

            synchronized (getni_lock) {

                if (last_getni_result != x) {

                    last_getni_result = x;

                    if (x.size() == 0 && old_network_interfaces == null) {

                    } else if (x.size() == 0) {

                        old_network_interfaces = null;

                        changed = true;

                    } else if (old_network_interfaces == null) {

                        Set<NetworkInterface> new_network_interfaces = new HashSet<NetworkInterface>();

                        new_network_interfaces.addAll(x);

                        old_network_interfaces = new_network_interfaces;

                        changed = true;

                    } else {

                        Set<NetworkInterface> new_network_interfaces = new HashSet<NetworkInterface>();

                        for (NetworkInterface ni : x) {

                            // NetworkInterface's "equals" method is based on ni name + addresses

                            if (!old_network_interfaces.contains(ni)) {

                                changed = true;
                            }

                            new_network_interfaces.add(ni);
                        }

                        if (old_network_interfaces.size() != new_network_interfaces.size()) {

                            changed = true;
                        }

                        old_network_interfaces = new_network_interfaces;
                    }

                    if (changed || force) {

                        boolean newV6 = false;
                        boolean newV4 = false;

                        Set<NetworkInterface> interfaces = old_network_interfaces;

                        long now = SystemTime.getMonotonousTime();

                        List<AddressHistoryRecord> a_history = new ArrayList<AddressHistoryRecord>();

                        if (interfaces != null) {
                            Iterator<NetworkInterface> it = interfaces.iterator();
                            while (it.hasNext()) {
                                NetworkInterface ni = it.next();
                                Enumeration addresses = ni.getInetAddresses();
                                while (addresses.hasMoreElements()) {
                                    InetAddress ia = (InetAddress) addresses.nextElement();

                                    a_history.add(new AddressHistoryRecord(ni, ia, now));

                                    if (ia.isLoopbackAddress()) {
                                        continue;
                                    }
                                    if (ia instanceof Inet6Address && !ia.isLinkLocalAddress()) {
                                        if (IPv6_enabled) {
                                            newV6 = true;
                                        }
                                    } else if (ia instanceof Inet4Address) {
                                        newV4 = true;
                                    }
                                }
                            }
                        }

                        synchronized (address_history) {

                            address_history_update_time = now;

                            for (AddressHistoryRecord entry : a_history) {

                                String name = entry.getAddress().getHostAddress();

                                AddressHistoryRecord existing = address_history.get(name);

                                if (existing == null) {

                                    address_history.put(name, entry);

                                } else {

                                    existing.setLastSeen(now);
                                }
                            }

                            Iterator<AddressHistoryRecord> it = address_history.values().iterator();

                            while (it.hasNext()) {

                                AddressHistoryRecord entry = it.next();

                                long age = now - entry.getLastSeen();

                                if (age > 10 * 60 * 1000) {

                                    it.remove();
                                }
                            }
                        }

                        supportsIPv4 = newV4;
                        supportsIPv6 = newV6;

                        Logger.log(new LogEvent(LOGID, "NetworkAdmin: ipv4 supported: " + supportsIPv4 + "; ipv6: " + supportsIPv6
                                + "; probing v6+nio functionality"));

                        if (newV6) {

                            ServerSocketChannel channel = ServerSocketChannel.open();

                            try {
                                channel.configureBlocking(false);
                                channel.socket().bind(new InetSocketAddress(anyLocalAddressIPv6, 0));
                                Logger.log(new LogEvent(LOGID, "NetworkAdmin: testing nio + ipv6 bind successful"));

                                supportsIPv6withNIO = true;
                            } catch (Exception e) {
                                Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING, "nio + ipv6 test failed", e));
                                supportsIPv6withNIO = false;
                            }

                            channel.close();
                        } else
                            supportsIPv6withNIO = false;

                        if (!first_time) {

                            Logger.log(new LogEvent(LOGID, "NetworkAdmin: network interfaces have changed"));
                        }

                        fire_stuff = true;
                    }
                }
            }

            if (fire_stuff) {

                firePropertyChange(NetworkAdmin.PR_NETWORK_INTERFACES);

                checkDefaultBindAddress(first_time);
            }
        } catch (Throwable e) {
        }

        return (changed);
    }

    public InetAddress getMultiHomedOutgoingRoundRobinBindAddress(InetAddress target) {
        InetAddress[] addresses = currentBindIPs;
        boolean v6 = target instanceof Inet6Address;
        int previous = (v6 ? roundRobinCounterV6 : roundRobinCounterV4) % addresses.length;
        InetAddress toReturn = null;

        int i = previous;

        do {
            i++;
            i %= addresses.length;
            if (target == null || (v6 && addresses[i] instanceof Inet6Address) || (!v6 && addresses[i] instanceof Inet4Address)) {
                toReturn = addresses[i];
                break;
            } else if (!v6 && addresses[i].isAnyLocalAddress()) {
                toReturn = anyLocalAddressIPv4;
                break;
            }
        }
        while (i != previous);

        if (v6)
            roundRobinCounterV6 = i;
        else
            roundRobinCounterV4 = i;
        return toReturn != null ? toReturn : (v6 ? localhostV6 : localhostV4);
    }

    public InetAddress[] getMultiHomedServiceBindAddresses(boolean nio) {
        InetAddress[] bindIPs = currentBindIPs;
        for (int i = 0; i < bindIPs.length; i++) {
            if (bindIPs[i].isAnyLocalAddress())
                return new InetAddress[] { nio && !supportsIPv6withNIO && bindIPs[i] instanceof Inet6Address ? anyLocalAddressIPv4 : bindIPs[i] };
        }
        return bindIPs;
    }

    public InetAddress getSingleHomedServiceBindAddress(int proto) {
        InetAddress[] addrs = currentBindIPs;
        if (proto == IP_PROTOCOL_VERSION_AUTO) {
            return addrs[0];
        } else {
            for (InetAddress addr : addrs) {

                if ((proto == IP_PROTOCOL_VERSION_REQUIRE_V4 && addr instanceof Inet4Address || addr.isAnyLocalAddress())
                        || (proto == IP_PROTOCOL_VERSION_REQUIRE_V6 && addr instanceof Inet6Address)) {

                    if (addr.isAnyLocalAddress()) {

                        if (proto == IP_PROTOCOL_VERSION_REQUIRE_V4) {

                            return (anyLocalAddressIPv4);

                        } else {

                            return (anyLocalAddressIPv6);
                        }
                    } else {

                        return (addr);
                    }
                }
            }
        }

        throw new UnsupportedAddressTypeException();
    }

    public InetAddress[] getAllBindAddresses(boolean include_wildcard) {
        if (include_wildcard) {

            return (currentBindIPs);

        } else {

            List<InetAddress> res = new ArrayList<InetAddress>();

            InetAddress[] bind_ips = currentBindIPs;

            for (InetAddress ip : bind_ips) {

                if (!ip.isAnyLocalAddress()) {

                    res.add(ip);
                }
            }

            return (res.toArray(new InetAddress[res.size()]));
        }
    }

    private InetAddress[] calcBindAddresses(final String addressString, boolean enforceBind) {
        ArrayList<InetAddress> addrs = new ArrayList<InetAddress>();

        Pattern addressSplitter = Pattern.compile(";");
        Pattern interfaceSplitter = Pattern.compile("[\\]\\[]");

        String[] tokens = addressSplitter.split(addressString);

        addressLoop: for (int i = 0; i < tokens.length; i++) {
            String currentAddress = tokens[i];

            currentAddress = currentAddress.trim();

            if (currentAddress.length() == 0) {
                continue;
            }

            InetAddress parsedAddress = null;

            try { // literal ipv4 or ipv6 address
                if (currentAddress.indexOf('.') != -1 || currentAddress.indexOf(':') != -1)
                    parsedAddress = InetAddress.getByName(currentAddress);
            } catch (Exception e) { // ignore, could be an interface name containing a ':'
            }

            if (parsedAddress != null) {
                try {
                    // allow wildcard address as 1st address, otherwise only interface addresses
                    if ((!parsedAddress.isAnyLocalAddress() || addrs.size() > 0) && NetworkInterface.getByInetAddress(parsedAddress) == null)
                        continue;
                } catch (SocketException e) {
                    Debug.printStackTrace(e);
                    continue;
                }
                addrs.add(parsedAddress);
                continue;
            }

            // interface name
            String[] ifaces = interfaceSplitter.split(currentAddress);

            NetworkInterface netInterface = null;
            try {
                netInterface = NetworkInterface.getByName(ifaces[0]);
            } catch (SocketException e) {
                e.printStackTrace(); // should not happen
            }
            if (netInterface == null)
                continue;

            Enumeration interfaceAddresses = netInterface.getInetAddresses();
            if (ifaces.length != 2)
                while (interfaceAddresses.hasMoreElements())
                    addrs.add((InetAddress) interfaceAddresses.nextElement());
            else {
                int selectedAddress = 0;
                try {
                    selectedAddress = Integer.parseInt(ifaces[1]);
                } catch (NumberFormatException e) {
                } // ignore, user could by typing atm
                for (int j = 0; interfaceAddresses.hasMoreElements(); j++, interfaceAddresses.nextElement())
                    if (j == selectedAddress) {
                        addrs.add((InetAddress) interfaceAddresses.nextElement());
                        continue addressLoop;
                    }
            }
        }

        if (!IPv6_enabled) {

            Iterator<InetAddress> it = addrs.iterator();

            while (it.hasNext()) {

                if (it.next() instanceof Inet6Address) {

                    it.remove();
                }
            }
        }

        if (addrs.size() < 1) {
            return new InetAddress[] { enforceBind ? localhostV4 : (hasIPV6Potential() ? anyLocalAddressIPv6 : anyLocalAddressIPv4) };
        }

        return (addrs.toArray(new InetAddress[addrs.size()]));

    }

    private String checkBindAddresses(boolean log_alerts) {
        Pattern addressSplitter = Pattern.compile(";");
        Pattern interfaceSplitter = Pattern.compile("[\\]\\[]");

        String bind_ips = COConfigurationManager.getStringParameter("Bind IP", "").trim();
        boolean enforceBind = COConfigurationManager.getBooleanParameter("Enforce Bind IP");

        if (enforceBind && bind_ips.length() == 0) {

            if (log_alerts) {

                Logger.log(new LogAlert(true, LogAlert.AT_WARNING, MessageText.getString("network.admin.bind.enforce.fail")));
            }
        }

        String[] tokens = addressSplitter.split(bind_ips);

        String failed_entries = "";

        for (int i = 0; i < tokens.length; i++) {

            String currentAddress = tokens[i];

            currentAddress = currentAddress.trim();

            if (currentAddress.length() == 0) {

                continue;
            }

            boolean ok = false;

            InetAddress parsedAddress = null;

            try {
                if (currentAddress.indexOf('.') != -1 || currentAddress.indexOf(':') != -1) {

                    parsedAddress = InetAddress.getByName(currentAddress);
                }
            } catch (Throwable e) {
            }

            if (parsedAddress != null) {

                try {
                    if (parsedAddress.isAnyLocalAddress() || NetworkInterface.getByInetAddress(parsedAddress) != null) {

                        ok = true;
                    }

                } catch (Throwable e) {
                }
            } else {

                // interface name

                String[] ifaces = interfaceSplitter.split(currentAddress);

                NetworkInterface netInterface = null;

                try {
                    netInterface = NetworkInterface.getByName(ifaces[0]);

                } catch (Throwable e) {
                }

                if (netInterface != null) {

                    Enumeration interfaceAddresses = netInterface.getInetAddresses();

                    if (ifaces.length != 2) {

                        ok = interfaceAddresses.hasMoreElements();

                    } else {

                        try {
                            int selectedAddress = Integer.parseInt(ifaces[1]);

                            for (int j = 0; interfaceAddresses.hasMoreElements(); j++, interfaceAddresses.nextElement()) {

                                if (j == selectedAddress) {

                                    ok = true;

                                    break;
                                }
                            }
                        } catch (Throwable e) {
                        }
                    }
                }
            }

            if (!ok) {

                failed_entries += (failed_entries.length() == 0 ? "" : ", ") + currentAddress;
            }
        }

        if (failed_entries.length() > 0) {

            if (log_alerts) {

                Logger.log(new LogAlert(true, LogAlert.AT_WARNING, "Bind IPs not resolved: " + failed_entries
                        + "\n\nSee Tools->Options->Connection->Advanced Network Settings"));
            }

            return (failed_entries);
        }

        return (null);
    }

    protected void checkDefaultBindAddress(boolean first_time) {
        boolean changed = false;
        String bind_ip = COConfigurationManager.getStringParameter("Bind IP", "").trim();

        boolean enforceBind = COConfigurationManager.getBooleanParameter("Enforce Bind IP");

        if (enforceBind) {

            if (bind_ip.length() == 0) {

                if (!logged_bind_force_issue) {

                    logged_bind_force_issue = true;

                    Debug.out("'Enforce IP Bindings' is selected but no bindings have been specified - ignoring force request!");
                }

                enforceBind = false;

            } else {

                logged_bind_force_issue = false;
            }
        }

        forceBind = enforceBind;

        InetAddress[] addrs = calcBindAddresses(bind_ip, enforceBind);
        changed = !Arrays.equals(currentBindIPs, addrs);
        if (changed) {
            currentBindIPs = addrs;
            if (!first_time) {

                String logmsg = "NetworkAdmin: default bind ip has changed to '";
                for (int i = 0; i < addrs.length; i++)
                    logmsg += (addrs[i] == null ? "none" : addrs[i].getHostAddress()) + (i < addrs.length ? ";" : "");
                logmsg += "'";
                Logger.log(new LogEvent(LOGID, logmsg));

                // if the user has removed a previous bind enforcement then re-activate the maybe-vpn
                // logic as they may be switching VPN

                if (bind_ip.length() == 0) {

                    clearMaybeVPNs();
                }
            }
            firePropertyChange(NetworkAdmin.PR_DEFAULT_BIND_ADDRESS);
        }
    }

    public String getNetworkInterfacesAsString() {
        Set interfaces = old_network_interfaces;

        if (interfaces == null) {

            return ("");
        }

        Iterator it = interfaces.iterator();
        StringBuilder sb = new StringBuilder(1024);
        while (it.hasNext()) {
            NetworkInterface ni = (NetworkInterface) it.next();
            Enumeration addresses = ni.getInetAddresses();
            sb.append(ni.getName());
            sb.append("\t(");
            sb.append(ni.getDisplayName());
            sb.append(")\n");
            int i = 0;
            while (addresses.hasMoreElements()) {
                InetAddress address = (InetAddress) addresses.nextElement();
                sb.append("\t");
                sb.append(ni.getName());
                sb.append("[");
                sb.append(i++);
                sb.append("]\t");
                sb.append((address).getHostAddress());
                sb.append("\n");
            }
        }
        return (sb.toString());
    }

    public boolean hasIPV4Potential() {
        return supportsIPv4;
    }

    public boolean hasIPV6Potential(boolean nio) {
        return nio ? supportsIPv6withNIO : supportsIPv6;
    }

    public InetAddress[] getBindableAddresses() {
        return (getBindableAddresses(false, false));
    }

    private InetAddress[] getBindableAddresses(boolean ignore_loopback, boolean ignore_link_local) {
        List<InetAddress> bindable = new ArrayList<InetAddress>();

        NetworkAdminNetworkInterface[] interfaces = NetworkAdmin.getSingleton().getInterfaces();

        for (NetworkAdminNetworkInterface intf : interfaces) {

            NetworkAdminNetworkInterfaceAddress[] addresses = intf.getAddresses();

            for (NetworkAdminNetworkInterfaceAddress address : addresses) {

                InetAddress a = address.getAddress();

                if ((ignore_loopback && a.isLoopbackAddress()) || (ignore_link_local && a.isLinkLocalAddress())) {

                } else {

                    if (canBind(a)) {

                        bindable.add(a);
                    }
                }
            }
        }

        return (bindable.toArray(new InetAddress[bindable.size()]));
    }

    protected boolean canBind(InetAddress bind_ip) {
        ServerSocketChannel ssc = null;

        try {
            ssc = ServerSocketChannel.open();

            ssc.socket().bind(new InetSocketAddress(bind_ip, 0), 16);

            return (true);

        } catch (Throwable e) {

            return (false);

        } finally {

            if (ssc != null) {

                try {
                    ssc.close();

                } catch (Throwable e) {

                    Debug.out(e);
                }
            }
        }
    }

    public int getBindablePort(int prefer_port)

    throws IOException {
        final int tries = 1024;

        Random random = new Random();

        for (int i = 1; i <= tries; i++) {

            int port;

            if (i == 1 && prefer_port != 0) {

                port = prefer_port;

            } else {

                port = i == tries ? 0 : random.nextInt(20000) + 40000;
            }

            ServerSocketChannel ssc = null;

            try {
                ssc = ServerSocketChannel.open();

                ssc.socket().setReuseAddress(true);

                bind(ssc, null, port);

                port = ssc.socket().getLocalPort();

                ssc.close();

                return (port);

            } catch (Throwable e) {

                if (ssc != null) {

                    try {
                        ssc.close();

                    } catch (Throwable f) {

                        Debug.printStackTrace(e);
                    }

                    ssc = null;
                }
            }
        }

        throw (new IOException("No bindable ports found"));
    }

    protected void bind(ServerSocketChannel ssc, InetAddress address, int port)

    throws IOException {
        if (address == null) {

            ssc.socket().bind(new InetSocketAddress(port), 1024);

        } else {

            ssc.socket().bind(new InetSocketAddress(address, port), 1024);
        }
    }

    public InetAddress guessRoutableBindAddress() {
        try {
            // see if we have a choice

            List local_addresses = new ArrayList();
            List non_local_addresses = new ArrayList();

            try {
                NetworkAdminNetworkInterface[] interfaces = getInterfaces();

                List possible = new ArrayList();

                for (int i = 0; i < interfaces.length; i++) {

                    NetworkAdminNetworkInterface intf = interfaces[i];

                    NetworkAdminNetworkInterfaceAddress[] addresses = intf.getAddresses();

                    for (int j = 0; j < addresses.length; j++) {

                        NetworkAdminNetworkInterfaceAddress address = addresses[j];

                        InetAddress ia = address.getAddress();

                        if (ia.isLoopbackAddress()) {

                            continue;
                        }

                        if (ia.isLinkLocalAddress() || ia.isSiteLocalAddress()) {

                            local_addresses.add(ia);

                        } else {

                            non_local_addresses.add(ia);
                        }

                        if ((hasIPV4Potential() && ia instanceof Inet4Address) || (hasIPV6Potential() && ia instanceof Inet6Address)) {

                            possible.add(ia);
                        }
                    }
                }

                if (possible.size() == 1) {

                    return ((InetAddress) possible.get(0));
                }
            } catch (Throwable e) {
            }

            // if we have a socks server then let's use a compatible address for it

            try {
                NetworkAdminSocksProxy[] socks = getSocksProxies();

                if (socks.length > 0) {

                    return (mapAddressToBindIP(InetAddress.getByName(socks[0].getHost())));
                }
            } catch (Throwable e) {
            }

            // next, same for nat devices

            try {
                NetworkAdminNATDevice[] nat = getNATDevices(AzureusCoreFactory.getSingleton());

                if (nat.length > 0) {

                    return (mapAddressToBindIP(nat[0].getAddress()));
                }
            } catch (Throwable e) {
            }

            try {
                final AESemaphore sem = new AESemaphore("NA:conTest");
                final InetAddress[] can_connect = { null };

                final int timeout = 10 * 1000;

                for (int i = 0; i < local_addresses.size(); i++) {

                    final InetAddress address = (InetAddress) local_addresses.get(i);

                    new AEThread2("NA:conTest", true) {
                        public void run() {
                            if (canConnectWithBind(address, timeout)) {

                                can_connect[0] = address;

                                sem.release();
                            }
                        }
                    }.start();
                }

                if (sem.reserve(timeout)) {

                    return (can_connect[0]);
                }
            } catch (Throwable e) {

            }

            // take a chance on any non local addresses we have

            if (non_local_addresses.size() > 0) {

                return (guessAddress(non_local_addresses));
            }

            // lastly, select local one at random

            if (local_addresses.size() > 0) {

                return (guessAddress(local_addresses));
            }

            // ho hum

            return (null);

        } catch (Throwable e) {

            Debug.printStackTrace(e);

            return (null);
        }
    }

    protected boolean canConnectWithBind(InetAddress bind_address, int timeout) {
        Socket socket = null;

        try {
            socket = new Socket();

            socket.bind(new InetSocketAddress(bind_address, 0));

            socket.setSoTimeout(timeout);

            socket.connect(new InetSocketAddress("www.google.com", 80), timeout);

            return (true);

        } catch (Throwable e) {

            return (false);

        } finally {

            if (socket != null) {

                try {
                    socket.close();

                } catch (Throwable f) {
                }
            }
        }
    }

    protected InetAddress mapAddressToBindIP(InetAddress address) {
        boolean[] address_bits = bytesToBits(address.getAddress());

        NetworkAdminNetworkInterface[] interfaces = getInterfaces();

        InetAddress best_bind_address = null;
        int best_prefix = 0;

        for (int i = 0; i < interfaces.length; i++) {

            NetworkAdminNetworkInterface intf = interfaces[i];

            NetworkAdminNetworkInterfaceAddress[] addresses = intf.getAddresses();

            for (int j = 0; j < addresses.length; j++) {

                NetworkAdminNetworkInterfaceAddress bind_address = addresses[j];

                InetAddress ba = bind_address.getAddress();

                byte[] bind_bytes = ba.getAddress();

                if (address_bits.length == bind_bytes.length) {

                    boolean[] bind_bits = bytesToBits(bind_bytes);

                    for (int k = 0; k < bind_bits.length; k++) {

                        if (address_bits[k] != bind_bits[k]) {

                            break;
                        }

                        if (k > best_prefix) {

                            best_prefix = k;

                            best_bind_address = ba;
                        }
                    }
                }
            }
        }

        return (best_bind_address);
    }

    protected boolean[] bytesToBits(byte[] bytes) {
        boolean[] res = new boolean[bytes.length * 8];

        for (int i = 0; i < bytes.length; i++) {

            byte b = bytes[i];

            for (int j = 0; j < 8; j++) {

                res[i * 8 + j] = (b & (byte) (0x01 << (7 - j))) != 0;
            }
        }

        return (res);
    }

    protected InetAddress guessAddress(List addresses) {
        // prioritise 192.168.0.* and 192.168.1.* as common
        // then ipv4 over ipv6

        for (int i = 0; i < addresses.size(); i++) {

            InetAddress address = (InetAddress) addresses.get(i);

            String str = address.getHostAddress();

            if (str.startsWith("192.168.0.") || str.startsWith("192.168.1.")) {

                return (address);
            }
        }

        for (int i = 0; i < addresses.size(); i++) {

            InetAddress address = (InetAddress) addresses.get(i);

            if (address instanceof Inet4Address) {

                return (address);
            }
        }

        for (int i = 0; i < addresses.size(); i++) {

            InetAddress address = (InetAddress) addresses.get(i);

            if (address instanceof Inet6Address) {

                return (address);
            }
        }

        if (addresses.size() > 0) {

            return ((InetAddress) addresses.get(0));
        }

        return (null);
    }

    private static InetAddress[] gdpa_lock = { null };
    private static AESemaphore gdpa_sem;
    private static long gdpa_last_fail;
    private static long gdpa_last_lookup;
    private static AESemaphore gdpa_initial_sem = new AESemaphore("gdpa:init");

    public InetAddress getDefaultPublicAddress() {
        return (getDefaultPublicAddress(false));
    }

    public InetAddress getDefaultPublicAddress(boolean peek) {
        final AESemaphore sem;

        synchronized (gdpa_lock) {

            long now = SystemTime.getMonotonousTime();

            if (gdpa_sem == null) {

                boolean do_lookup = true;

                if (peek) {

                    if (gdpa_last_lookup != 0 && now - gdpa_last_lookup <= 60 * 1000) {

                        do_lookup = false;
                    }
                }

                if (do_lookup) {

                    gdpa_last_lookup = now;

                    gdpa_sem = sem = new AESemaphore("getDefaultPublicAddress");

                    new AEThread2("getDefaultPublicAddress") {
                        public void run() {
                            InetAddress address = null;

                            try {
                                Utilities utils = PluginInitializer.getDefaultInterface().getUtilities();

                                address = utils.getPublicAddress();

                                if (address == null) {

                                    address = utils.getPublicAddress(true);
                                }
                            } catch (Throwable e) {

                            } finally {

                                synchronized (gdpa_lock) {

                                    gdpa_lock[0] = address;

                                    sem.releaseForever();

                                    gdpa_sem = null;

                                    gdpa_initial_sem.releaseForever();
                                }
                            }
                        }
                    }.start();
                } else {

                    sem = null; // no lookup this time around - we're peeking
                }
            } else {

                sem = gdpa_sem;
            }

            if (gdpa_last_fail != 0 && SystemTime.getMonotonousTime() - gdpa_last_fail < 5 * 60 * 1000) {

                return (gdpa_lock[0]);
            }
        }

        if (peek) {

            // we're doing a peek and don't want to wait

            gdpa_initial_sem.reserve(10 * 1000);

            synchronized (gdpa_lock) {

                return (gdpa_lock[0]);
            }
        } else {
            // in case things block - yes, they can do :(

            boolean worked = sem.reserve(10 * 1000);

            synchronized (gdpa_lock) {

                if (worked) {

                    gdpa_last_fail = 0;

                } else {

                    gdpa_initial_sem.releaseForever();

                    gdpa_last_fail = SystemTime.getMonotonousTime();
                }

                return (gdpa_lock[0]);
            }
        }
    }

    @Override
    public InetAddress getDefaultPublicAddressV6() {
        return (getDefaultPublicAddressV6(false));
    }

    @Override
    public InetAddress getDefaultPublicAddressV6(boolean peek) {
        if (!supportsIPv6)
            return null;

        // check bindings first
        for (InetAddress addr : currentBindIPs) {
            // found a specific bind address, use that one
            if (AddressUtils.isGlobalAddressV6(addr)) {
                return addr;
            }
        }

        for (InetAddress addr : currentBindIPs) {
            // found v6 any-local address, check interfaces for a best match
            if (addr instanceof Inet6Address && addr.isAnyLocalAddress()) {
                ArrayList<InetAddress> addrs = new ArrayList<InetAddress>();
                for (NetworkInterface iface : old_network_interfaces)
                    addrs.addAll(Collections.list(iface.getInetAddresses()));

                return AddressUtils.pickBestGlobalV6Address(addrs);
            }
        }

        return null;
    }

    public boolean hasDHTIPV6() {
        if (hasIPV6Potential(false)) {

            InetAddress v6 = getDefaultPublicAddressV6();

            if (v6 == null) {

                return (false);
            }

            if (Constants.IS_CVS_VERSION) {

                return (true);

            } else {

                return (!AddressUtils.isTeredo(v6));
            }
        }

        return (false);
    }

    protected void firePropertyChange(String property) {
        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((NetworkAdminPropertyChangeListener) it.next()).propertyChanged(property);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public NetworkAdminNetworkInterface[] getInterfaces() {
        Set interfaces = old_network_interfaces;

        if (interfaces == null) {

            return (new NetworkAdminNetworkInterface[0]);
        }

        NetworkAdminNetworkInterface[] res = new NetworkAdminNetworkInterface[interfaces.size()];

        Iterator it = interfaces.iterator();

        int pos = 0;

        while (it.hasNext()) {

            NetworkInterface ni = (NetworkInterface) it.next();

            res[pos++] = new networkInterface(ni);
        }

        return (res);
    }

    public NetworkAdminProtocol[] getOutboundProtocols(AzureusCore azureus_core) {
        NetworkAdminProtocol[] res =
                { new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_HTTP),
                        new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_TCP),
                        new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_UDP), };

        return (res);
    }

    public NetworkAdminProtocol createInboundProtocol(AzureusCore azureus_core, int type, int port) {
        return (new NetworkAdminProtocolImpl(azureus_core, type, port));
    }

    public NetworkAdminProtocol[] getInboundProtocols(AzureusCore azureus_core) {
        List protocols = new ArrayList();

        TCPNetworkManager tcp_manager = TCPNetworkManager.getSingleton();

        if (tcp_manager.isTCPListenerEnabled()) {

            protocols.add(new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_TCP, tcp_manager.getTCPListeningPortNumber()));
        }

        UDPNetworkManager udp_manager = UDPNetworkManager.getSingleton();

        int done_udp = -1;

        if (udp_manager.isUDPListenerEnabled()) {

            protocols.add(new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_UDP, done_udp =
                    udp_manager.getUDPListeningPortNumber()));
        }

        if (udp_manager.isUDPNonDataListenerEnabled()) {

            int port = udp_manager.getUDPNonDataListeningPortNumber();

            if (port != done_udp) {

                protocols.add(new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_UDP, done_udp =
                        udp_manager.getUDPNonDataListeningPortNumber()));

            }
        }

        HTTPNetworkManager http_manager = HTTPNetworkManager.getSingleton();

        if (http_manager.isHTTPListenerEnabled()) {

            protocols.add(new NetworkAdminProtocolImpl(azureus_core, NetworkAdminProtocol.PT_HTTP, http_manager.getHTTPListeningPortNumber()));
        }

        return ((NetworkAdminProtocol[]) protocols.toArray(new NetworkAdminProtocol[protocols.size()]));
    }

    public InetAddress testProtocol(NetworkAdminProtocol protocol)

    throws NetworkAdminException {
        return (protocol.test(null));
    }

    @Override
    public boolean isSocksActive() {
        Proxy proxy = AEProxySelectorFactory.getSelector().getActiveProxy();

        return (proxy != null && proxy.type() == Proxy.Type.SOCKS);
    }

    public NetworkAdminSocksProxy createSocksProxy(String host, int port, String username, String password) {
        return (new NetworkAdminSocksProxyImpl(host, "" + port, username, password));
    }

    public NetworkAdminSocksProxy[] getSocksProxies() {
        String host = System.getProperty("socksProxyHost", "").trim();
        String port = System.getProperty("socksProxyPort", "").trim();

        String user = System.getProperty("java.net.socks.username", "").trim();
        String password = System.getProperty("java.net.socks.password", "").trim();

        List res = new ArrayList();

        NetworkAdminSocksProxyImpl p1 = new NetworkAdminSocksProxyImpl(host, port, user, password);

        if (p1.isConfigured()) {

            res.add(p1);
        }

        if (COConfigurationManager.getBooleanParameter("Proxy.Data.Enable") && !COConfigurationManager.getBooleanParameter("Proxy.Data.Same")) {

            host = COConfigurationManager.getStringParameter("Proxy.Data.Host");
            port = COConfigurationManager.getStringParameter("Proxy.Data.Port");
            user = COConfigurationManager.getStringParameter("Proxy.Data.Username");

            if (user.trim().equalsIgnoreCase("<none>")) {
                user = "";
            }
            password = COConfigurationManager.getStringParameter("Proxy.Data.Password");

            NetworkAdminSocksProxyImpl p2 = new NetworkAdminSocksProxyImpl(host, port, user, password);

            if (p2.isConfigured()) {

                res.add(p2);
            }
        }

        return ((NetworkAdminSocksProxy[]) res.toArray(new NetworkAdminSocksProxy[res.size()]));
    }

    public NetworkAdminHTTPProxy getHTTPProxy() {
        NetworkAdminHTTPProxyImpl res = new NetworkAdminHTTPProxyImpl();

        if (!res.isConfigured()) {

            res = null;
        }

        return (res);
    }

    public NetworkAdminNATDevice[] getNATDevices(AzureusCore azureus_core) {
        List<NetworkAdminNATDeviceImpl> devices = new ArrayList<NetworkAdminNATDeviceImpl>();

        try {

            PluginInterface upnp_pi = azureus_core.getPluginManager().getPluginInterfaceByClass(UPnPPlugin.class);

            if (upnp_pi != null) {

                UPnPPlugin upnp = (UPnPPlugin) upnp_pi.getPlugin();

                UPnPPluginService[] services = upnp.getServices();

                for (UPnPPluginService service : services) {

                    NetworkAdminNATDeviceImpl dev = new NetworkAdminNATDeviceImpl(service);

                    boolean same = false;

                    for (NetworkAdminNATDeviceImpl d : devices) {

                        if (d.sameAs(dev)) {

                            same = true;

                            break;
                        }
                    }

                    if (!same) {

                        devices.add(dev);
                    }
                }
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }

        return ((NetworkAdminNATDevice[]) devices.toArray(new NetworkAdminNATDevice[devices.size()]));
    }

    public NetworkAdminASN getCurrentASN() {
        List asns = COConfigurationManager.getListParameter("ASN Details", new ArrayList());

        if (asns.size() == 0) {

            // migration from when we only persisted a single AS

            String as = "";
            String asn = "";
            String bgp = "";

            try {
                as = COConfigurationManager.getStringParameter("ASN AS");
                asn = COConfigurationManager.getStringParameter("ASN ASN");
                bgp = COConfigurationManager.getStringParameter("ASN BGP");

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }

            COConfigurationManager.removeParameter("ASN AS");
            COConfigurationManager.removeParameter("ASN ASN");
            COConfigurationManager.removeParameter("ASN BGP");
            COConfigurationManager.removeParameter("ASN Autocheck Performed Time");

            asns.add(ASNToMap(new NetworkAdminASNImpl(as, asn, bgp)));

            COConfigurationManager.setParameter("ASN Details", asns);
        }

        if (asns.size() > 0) {

            Map m = (Map) asns.get(0);

            return (ASNFromMap(m));
        }

        return (new NetworkAdminASNImpl("", "", ""));
    }

    protected Map ASNToMap(NetworkAdminASNImpl x) {
        Map m = new HashMap();

        byte[] as = new byte[0];
        byte[] asn = new byte[0];
        byte[] bgp = new byte[0];

        try {
            as = x.getAS().getBytes("UTF-8");
            asn = x.getASName().getBytes("UTF-8");
            bgp = x.getBGPPrefix().getBytes("UTF-8");

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }

        m.put("as", as);
        m.put("name", asn);
        m.put("bgp", bgp);

        return (m);
    }

    protected NetworkAdminASNImpl ASNFromMap(Map m) {
        String as = "";
        String asn = "";
        String bgp = "";

        try {
            as = new String((byte[]) m.get("as"), "UTF-8");
            asn = new String((byte[]) m.get("name"), "UTF-8");
            bgp = new String((byte[]) m.get("bgp"), "UTF-8");

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }

        return (new NetworkAdminASNImpl(as, asn, bgp));
    }

    public NetworkAdminASN lookupCurrentASN(InetAddress address)

    throws NetworkAdminException {
        NetworkAdminASN current = getCurrentASN();

        if (current.matchesCIDR(address)) {

            return (current);
        }

        List asns = COConfigurationManager.getListParameter("ASN Details", new ArrayList());

        for (int i = 0; i < asns.size(); i++) {

            Map m = (Map) asns.get(i);

            NetworkAdminASN x = ASNFromMap(m);

            if (x.matchesCIDR(address)) {

                asns.remove(i);

                asns.add(0, m);

                firePropertyChange(PR_AS);

                return (x);
            }
        }

        if (asn_ips_checked.contains(address)) {

            return (current);
        }

        long now = SystemTime.getCurrentTime();

        if (now < last_asn_lookup_time || now - last_asn_lookup_time > ASN_MIN_CHECK) {

            last_asn_lookup_time = now;

            NetworkAdminASNLookupImpl lookup = new NetworkAdminASNLookupImpl(address);

            NetworkAdminASNImpl x = lookup.lookup();

            asn_ips_checked.add(address);

            asns.add(0, ASNToMap(x));

            firePropertyChange(PR_AS);

            return (x);
        }

        return (current);
    }

    public NetworkAdminASN lookupASN(InetAddress address)

    throws NetworkAdminException {
        NetworkAdminASN existing = getFromASHistory(address);

        if (existing != null) {

            return (existing);
        }

        NetworkAdminASNLookupImpl lookup = new NetworkAdminASNLookupImpl(address);

        NetworkAdminASNImpl result = lookup.lookup();

        addToASHistory(result);

        return (result);
    }

    public void lookupASN(final InetAddress address, final NetworkAdminASNListener listener) {
        synchronized (async_asn_history) {

            NetworkAdminASN existing = async_asn_history.get(address);

            if (existing != null) {

                listener.success(existing);
            }
        }

        int queue_size = async_asn_dispacher.getQueueSize();

        if (queue_size >= MAX_ASYNC_ASN_LOOKUPS) {

            listener.failed(new NetworkAdminException("Too many outstanding lookups"));

        } else {

            async_asn_dispacher.dispatch(new AERunnable() {
                public void runSupport() {
                    synchronized (async_asn_history) {

                        NetworkAdminASN existing = async_asn_history.get(address);

                        if (existing != null) {

                            listener.success(existing);

                            return;
                        }
                    }

                    try {
                        NetworkAdminASNLookupImpl lookup = new NetworkAdminASNLookupImpl(address);

                        NetworkAdminASNImpl result = lookup.lookup();

                        synchronized (async_asn_history) {

                            async_asn_history.put(address, result);
                        }

                        listener.success(result);

                    } catch (NetworkAdminException e) {

                        listener.failed(e);

                    } catch (Throwable e) {

                        listener.failed(new NetworkAdminException("lookup failed", e));
                    }
                }
            });
        }
    }

    protected void addToASHistory(NetworkAdminASN asn) {
        synchronized (as_history) {

            boolean found = false;

            for (int i = 0; i < as_history.size(); i++) {

                NetworkAdminASN x = (NetworkAdminASN) as_history.get(i);

                if (asn.getAS().equals(x.getAS())) {

                    found = true;

                    break;
                }
            }

            if (!found) {

                as_history.add(asn);

                if (as_history.size() > 256) {

                    as_history.remove(0);
                }
            }
        }
    }

    protected NetworkAdminASN getFromASHistory(InetAddress address) {
        synchronized (as_history) {

            for (int i = 0; i < as_history.size(); i++) {

                NetworkAdminASN x = (NetworkAdminASN) as_history.get(i);

                if (x.matchesCIDR(address)) {

                    return (x);
                }
            }
        }

        return (null);
    }

    public void runInitialChecks(AzureusCore azureus_core) {
        AZInstanceManager i_man = azureus_core.getInstanceManager();

        final AZInstance my_instance = i_man.getMyInstance();

        i_man.addListener(new AZInstanceManagerListener() {
            private InetAddress external_address;

            public void instanceFound(AZInstance instance) {
            }

            public void instanceChanged(AZInstance instance) {
                if (instance == my_instance) {

                    InetAddress address = instance.getExternalAddress();

                    if (external_address == null || !external_address.equals(address)) {

                        external_address = address;

                        try {
                            lookupCurrentASN(address);

                        } catch (Throwable e) {

                            Debug.printStackTrace(e);
                        }
                    }
                }
            }

            public void instanceLost(AZInstance instance) {
            }

            public void instanceTracked(AZInstanceTracked instance) {
            }
        });

        if (COConfigurationManager.getBooleanParameter("Proxy.Check.On.Start")) {

            NetworkAdminSocksProxy[] socks = getSocksProxies();

            for (int i = 0; i < socks.length; i++) {

                NetworkAdminSocksProxy sock = socks[i];

                try {
                    sock.getVersionsSupported();

                } catch (Throwable e) {

                    Debug.printStackTrace(e);

                    Logger.log(new LogAlert(true, LogAlert.AT_WARNING, "Socks proxy " + sock.getName() + " check failed: "
                            + Debug.getNestedExceptionMessage(e)));
                }
            }

            NetworkAdminHTTPProxy http_proxy = getHTTPProxy();

            if (http_proxy != null) {

                try {

                    http_proxy.getDetails();

                } catch (Throwable e) {

                    Debug.printStackTrace(e);

                    Logger.log(new LogAlert(true, LogAlert.AT_WARNING, "HTTP proxy " + http_proxy.getName() + " check failed: "
                            + Debug.getNestedExceptionMessage(e)));
                }
            }
        }

        if (COConfigurationManager.getBooleanParameter("Check Bind IP On Start")) {

            checkBindAddresses(true);
        }

        NetworkAdminSpeedTestScheduler nast = NetworkAdminSpeedTestSchedulerImpl.getInstance();

        nast.initialise();
    }

    public boolean canTraceRoute() {
        PlatformManager pm = PlatformManagerFactory.getPlatformManager();

        return (pm.hasCapability(PlatformManagerCapabilities.TraceRouteAvailability));
    }

    public NetworkAdminNode[] getRoute(InetAddress interface_address, InetAddress target, final int max_millis,
            final NetworkAdminRouteListener listener)

    throws NetworkAdminException {
        PlatformManager pm = PlatformManagerFactory.getPlatformManager();

        if (!canTraceRoute()) {

            throw (new NetworkAdminException("No trace-route capability on platform"));
        }

        final List nodes = new ArrayList();

        try {
            pm.traceRoute(interface_address, target, new PlatformManagerPingCallback() {
                private long start_time = SystemTime.getCurrentTime();

                public boolean reportNode(int distance, InetAddress address, int millis) {
                    boolean timeout = false;

                    if (max_millis >= 0) {

                        long now = SystemTime.getCurrentTime();

                        if (now < start_time) {

                            start_time = now;
                        }

                        if (now - start_time >= max_millis) {

                            timeout = true;
                        }
                    }

                    NetworkAdminNode node = null;

                    if (address != null) {

                        node = new networkNode(address, distance, millis);

                        nodes.add(node);
                    }

                    boolean result;

                    if (listener == null) {

                        result = true;

                    } else {

                        if (node == null) {

                            result = listener.timeout(distance);

                        } else {

                            result = listener.foundNode(node, distance, millis);
                        }
                    }

                    return (result && !timeout);
                }
            });
        } catch (PlatformManagerException e) {

            throw (new NetworkAdminException("trace-route failed", e));
        }

        return ((NetworkAdminNode[]) nodes.toArray(new NetworkAdminNode[nodes.size()]));
    }

    public boolean canPing() {
        PlatformManager pm = PlatformManagerFactory.getPlatformManager();

        return (pm.hasCapability(PlatformManagerCapabilities.PingAvailability));
    }

    public NetworkAdminNode pingTarget(InetAddress interface_address, InetAddress target, final int max_millis,
            final NetworkAdminRouteListener listener)

    throws NetworkAdminException {
        PlatformManager pm = PlatformManagerFactory.getPlatformManager();

        if (!canPing()) {

            throw (new NetworkAdminException("No ping capability on platform"));
        }

        final NetworkAdminNode[] nodes = { null };

        try {
            pm.ping(interface_address, target, new PlatformManagerPingCallback() {
                private long start_time = SystemTime.getCurrentTime();

                public boolean reportNode(int distance, InetAddress address, int millis) {
                    boolean timeout = false;

                    if (max_millis >= 0) {

                        long now = SystemTime.getCurrentTime();

                        if (now < start_time) {

                            start_time = now;
                        }

                        if (now - start_time >= max_millis) {

                            timeout = true;
                        }
                    }

                    NetworkAdminNode node = null;

                    if (address != null) {

                        node = new networkNode(address, distance, millis);

                        nodes[0] = node;
                    }

                    boolean result;

                    if (listener == null) {

                        result = false;

                    } else {

                        if (node == null) {

                            result = listener.timeout(distance);

                        } else {

                            result = listener.foundNode(node, distance, millis);
                        }
                    }

                    return (result && !timeout);
                }
            });
        } catch (PlatformManagerException e) {

            throw (new NetworkAdminException("ping failed", e));
        }

        return (nodes[0]);
    }

    public void getRoutes(final InetAddress target, final int max_millis, final NetworkAdminRoutesListener listener)

    throws NetworkAdminException {
        final List sems = new ArrayList();
        final List traces = new ArrayList();

        NetworkAdminNetworkInterface[] interfaces = getInterfaces();

        for (int i = 0; i < interfaces.length; i++) {

            NetworkAdminNetworkInterface interf = (NetworkAdminNetworkInterface) interfaces[i];

            NetworkAdminNetworkInterfaceAddress[] addresses = interf.getAddresses();

            for (int j = 0; j < addresses.length; j++) {

                final NetworkAdminNetworkInterfaceAddress address = addresses[j];

                InetAddress ia = address.getAddress();

                if (ia.isLoopbackAddress() || ia instanceof Inet6Address) {

                    // ignore

                } else {

                    final AESemaphore sem = new AESemaphore("parallelRouter");

                    final List trace = new ArrayList();

                    sems.add(sem);

                    traces.add(trace);

                    new AEThread2("parallelRouter", true) {
                        public void run() {
                            try {
                                address.getRoute(target, 30000, new NetworkAdminRouteListener() {
                                    public boolean foundNode(NetworkAdminNode node, int distance, int rtt) {
                                        trace.add(node);

                                        NetworkAdminNode[] route = new NetworkAdminNode[trace.size()];

                                        trace.toArray(route);

                                        return (listener.foundNode(address, route, distance, rtt));
                                    }

                                    public boolean timeout(int distance) {
                                        NetworkAdminNode[] route = new NetworkAdminNode[trace.size()];

                                        trace.toArray(route);

                                        return (listener.timeout(address, route, distance));
                                    }
                                });

                            } catch (Throwable e) {

                                e.printStackTrace();

                            } finally {

                                sem.release();
                            }
                        }
                    }.start();
                }
            }
        }

        for (int i = 0; i < sems.size(); i++) {

            ((AESemaphore) sems.get(i)).reserve();
        }
    }

    public void pingTargets(final InetAddress target, final int max_millis, final NetworkAdminRoutesListener listener)

    throws NetworkAdminException {
        final List sems = new ArrayList();
        final List traces = new ArrayList();

        NetworkAdminNetworkInterface[] interfaces = getInterfaces();

        for (int i = 0; i < interfaces.length; i++) {

            NetworkAdminNetworkInterface interf = (NetworkAdminNetworkInterface) interfaces[i];

            NetworkAdminNetworkInterfaceAddress[] addresses = interf.getAddresses();

            for (int j = 0; j < addresses.length; j++) {

                final NetworkAdminNetworkInterfaceAddress address = addresses[j];

                InetAddress ia = address.getAddress();

                if (ia.isLoopbackAddress() || ia instanceof Inet6Address) {

                    // ignore

                } else {

                    final AESemaphore sem = new AESemaphore("parallelPinger");

                    final List trace = new ArrayList();

                    sems.add(sem);

                    traces.add(trace);

                    new AEThread2("parallelPinger", true) {
                        public void run() {
                            try {
                                address.pingTarget(target, 30000, new NetworkAdminRouteListener() {
                                    public boolean foundNode(NetworkAdminNode node, int distance, int rtt) {
                                        trace.add(node);

                                        NetworkAdminNode[] route = new NetworkAdminNode[trace.size()];

                                        trace.toArray(route);

                                        return (listener.foundNode(address, route, distance, rtt));
                                    }

                                    public boolean timeout(int distance) {
                                        NetworkAdminNode[] route = new NetworkAdminNode[trace.size()];

                                        trace.toArray(route);

                                        return (listener.timeout(address, route, distance));
                                    }
                                });

                            } catch (Throwable e) {

                                e.printStackTrace();

                            } finally {

                                sem.release();
                            }
                        }
                    }.start();
                }
            }
        }

        for (int i = 0; i < sems.size(); i++) {

            ((AESemaphore) sems.get(i)).reserve();
        }
    }

    @Override
    public boolean mustBind() {
        return (forceBind);
    }

    public boolean hasMissingForcedBind() {
        Object[] status = getBindingStatus();

        return ((Integer) status[0] == BS_ERROR);
    }

    // for the icon

    public static final int BS_INACTIVE = 0;
    public static final int BS_OK = 1;
    public static final int BS_WARNING = 2;
    public static final int BS_ERROR = 3;

    private long bs_last_calc = 0;
    private Object[] bs_last_value = null;

    public Object[] getBindingStatus() {
        long now = SystemTime.getMonotonousTime();

        if (bs_last_value != null && now - bs_last_calc < 30 * 1000) {

            return (bs_last_value);
        }

        String bind_ips = COConfigurationManager.getStringParameter("Bind IP", "").trim();

        if (bind_ips.length() == 0) {

            return (new Object[] { BS_INACTIVE, "" });
        }

        boolean enforceBind = COConfigurationManager.getBooleanParameter("Enforce Bind IP");

        String missing = checkBindAddresses(false);

        InetAddress[] binds = getAllBindAddresses(false);

        List<InetAddress> bindable = new ArrayList<InetAddress>();
        List<InetAddress> unbindable = new ArrayList<InetAddress>();

        for (InetAddress b : binds) {

            if (canBind(b)) {

                bindable.add(b);

            } else {

                if (!(b.isLoopbackAddress() || b.isLinkLocalAddress())) {

                    unbindable.add(b);
                }
            }
        }

        Set<NetworkConnectionBase> connections = NetworkManager.getSingleton().getConnections();

        Map<InetAddress, int[]> lookup_map = new HashMap<InetAddress, int[]>();

        for (NetworkConnectionBase connection : connections) {

            TransportBase tb = connection.getTransportBase();

            if (tb instanceof Transport) {

                Transport transport = (Transport) tb;

                TransportStartpoint start = transport.getTransportStartpoint();

                if (start != null) {

                    InetSocketAddress socket_address = start.getProtocolStartpoint().getAddress();

                    if (socket_address != null) {

                        InetAddress address = socket_address.getAddress();

                        int[] counts = lookup_map.get(address);

                        if (counts == null) {

                            counts = new int[2];

                            lookup_map.put(address, counts);
                        }

                        if (connection.isIncoming()) {

                            counts[0]++;

                        } else {

                            counts[1]++;
                        }
                    }
                }
            }
        }

        int status = BS_OK;

        if (unbindable.size() > 0 || missing != null) {

            status = enforceBind ? BS_ERROR : BS_WARNING;
        }

        String str =
                MessageText.getString("network.admin.binding.state", new String[] { getString(bindable),
                        MessageText.getString(enforceBind ? "GeneralView.yes" : "GeneralView.no").toLowerCase() });

        if (unbindable.size() > 0) {
            str += "\nUnbindable: " + getString(unbindable);
        }

        if (missing != null) {
            str += "\n" + MessageText.getString("label.missing") + ": " + missing;
        }

        boolean unbound_connections = false;

        if (lookup_map.size() == 0) {

            str += "\n" + MessageText.getString("label.no.connections");

        } else {

            String con_str = "";

            for (Map.Entry<InetAddress, int[]> entry : lookup_map.entrySet()) {

                InetAddress address = entry.getKey();
                int[] counts = entry.getValue();

                String s;

                if (address.isAnyLocalAddress()) {

                    s = "*";

                    unbound_connections = true;

                } else {

                    s = address.getHostAddress();

                    if (!bindable.contains(address)) {

                        unbound_connections = true;
                    }
                }

                s += " - " + MessageText.getString("label.in") + "=" + counts[0] + ", " + MessageText.getString("label.out") + "=" + counts[1];

                con_str += (con_str.length() == 0 ? "" : "; ") + s.toLowerCase();
            }

            str += "\n" + MessageText.getString("label.connections") + ": " + con_str;
        }

        if (unbound_connections) {

            status = enforceBind ? BS_ERROR : BS_WARNING;
        }

        bs_last_value = new Object[] { status, str };
        bs_last_calc = now;

        return (bs_last_value);
    }

    private String getString(List<InetAddress> addresses) {
        if (addresses.size() == 0) {

            return ("<none>");
        }

        String str = "";

        for (InetAddress address : addresses) {

            str += (str.length() == 0 ? "" : ", ") + address.getHostAddress();
        }

        return (str);
    }

    private void checkConnectionRoutes() {
        // System.out.println( "Checking connection routes" );

        if (getAllBindAddresses(false).length > 0) {

            // System.out.println( "    Bind IP found" );

            return;
        }

        Set<NetworkConnectionBase> connections = NetworkManager.getSingleton().getConnections();

        // check if all outgoing TCP connections are being routed through the same interface

        boolean found_wildcard = false;
        int tcp_found = 0;

        Map<InetAddress, Object[]> lookup_map = new HashMap<InetAddress, Object[]>();
        Map<String, Object[]> bind_map = new HashMap<String, Object[]>();

        for (NetworkConnectionBase connection : connections) {

            if (!connection.isIncoming()) {

                TransportBase tb = connection.getTransportBase();

                if (tb instanceof Transport) {

                    Transport transport = (Transport) tb;

                    if (transport.isTCP()) {

                        TransportStartpoint start = transport.getTransportStartpoint();

                        if (start != null) {

                            InetSocketAddress socket_address = start.getProtocolStartpoint().getAddress();

                            if (socket_address != null) {

                                tcp_found++;

                                InetAddress address = socket_address.getAddress();

                                if (address.isAnyLocalAddress()) {

                                    found_wildcard = true;

                                } else {

                                    Object[] details = lookup_map.get(address);

                                    if (details == null) {

                                        if (!lookup_map.containsKey(address)) {

                                            details = getInterfaceForAddress(address);

                                            lookup_map.put(address, details);
                                        }
                                    }

                                    if (details != null && details[0] instanceof NetworkInterface) {

                                        NetworkInterface intf = (NetworkInterface) details[0];

                                        InetAddress intf_address = details.length == 1 ? null : (InetAddress) details[1];

                                        String key = intf.getName() + "/" + intf_address;

                                        Object[] entry = bind_map.get(key);

                                        if (entry == null) {

                                            entry = new Object[] { new int[1], details };

                                            bind_map.put(key, entry);
                                        }

                                        ((int[]) entry[0])[0]++;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if (tcp_found > 8) {

            if (found_wildcard && bind_map.size() == 0) {

                // unfortunately we don't always get access to the locally bound
                // interfaces, so for the case where we have nothing useful look
                // to see if there are more than one interface and if one looks like
                // a vpn. I did try an explicit 'connect with bind' to see if I could
                // work out if just one was routing but unfortunately this didn't
                // work (with open-vpn at least) as it still permits explicit connections
                // through non-vpn interface - grrr!

                // System.out.println( "Everything wildcard" );

                InetAddress[] bindable_addresses = getBindableAddresses(true, true);

                // System.out.println( "Bindable=" + bindable_addresses.length );

                if (bindable_addresses.length > 1) {

                    Map<String, NetworkInterface> intf_map = new HashMap<String, NetworkInterface>();

                    for (InetAddress address : bindable_addresses) {

                        Object[] details = getInterfaceForAddress(address);

                        if (details != null && details[0] instanceof NetworkInterface) {

                            NetworkInterface intf = (NetworkInterface) details[0];

                            intf_map.put(intf.getName(), intf);
                        }
                    }

                    if (intf_map.size() > 1) {

                        int eth_like = 0;

                        Map<String, NetworkInterface> vpn_like = new HashMap<String, NetworkInterface>();

                        for (Map.Entry<String, NetworkInterface> entry : intf_map.entrySet()) {

                            int type = categoriseIntf(entry.getValue());

                            if (type == 1) {

                                eth_like++;

                            } else if (type == 2) {

                                vpn_like.put(entry.getKey(), entry.getValue());
                            }
                        }

                        if (vpn_like.size() == 1 && eth_like > 0) {

                            maybeVPN(vpn_like.values().iterator().next());
                        }
                    }
                }
            } else if (!found_wildcard && bind_map.size() == 1) {

                Object[] bound_details = (Object[]) bind_map.values().iterator().next()[1];

                NetworkInterface bound_intf = (NetworkInterface) bound_details[0];

                // System.out.println( "All outgoing TCP bound to same: " + bound_intf );

                // maybeVPN( bound_intf );

                int bound_type = categoriseIntf(bound_intf);

                if (bound_type == 2) {

                    if (!maybeVPNDone(bound_intf)) {

                        InetAddress[] bindable_addresses = getBindableAddresses(true, true);

                        if (bindable_addresses.length > 1) {

                            int eth_like = 0;
                            int vpn_like = 0;

                            for (InetAddress address : bindable_addresses) {

                                Object[] details = getInterfaceForAddress(address);

                                if (details != null && details[0] instanceof NetworkInterface) {

                                    NetworkInterface intf = (NetworkInterface) details[0];

                                    if (intf != bound_intf) {

                                        int type = categoriseIntf(intf);

                                        if (type == 1) {

                                            eth_like++;

                                        } else if (type == 2) {

                                            vpn_like++;
                                        }
                                    }
                                }
                            }

                            if (vpn_like == 0 && eth_like > 0) {

                                maybeVPN(bound_intf);
                            }
                        }
                    }
                }
            }
        }
    }

    private void clearMaybeVPNs() {
        Set<String> keys = COConfigurationManager.getDefinedParameters();

        for (String key : keys) {

            if (key.startsWith("network.admin.maybe.vpn.done.")) {

                COConfigurationManager.removeParameter(key);
            }
        }
    }

    private boolean maybeVPNDone(NetworkInterface intf) {
        return (COConfigurationManager.getBooleanParameter("network.admin.maybe.vpn.done." + getConfigKey(intf), false));
    }

    private void maybeVPN(final NetworkInterface intf) {
        final UIManager ui_manager = StaticUtilities.getUIManager(5 * 1000);

        if (ui_manager == null) {

            return;
        }

        // might have done in the meantime

        if (maybeVPNDone(intf)) {

            return;
        }

        COConfigurationManager.setParameter("network.admin.maybe.vpn.done." + getConfigKey(intf), true);

        new AEThread2("NetworkAdmin:vpn?") {
            public void run() {
                String msg_details =
                        MessageText.getString("network.admin.maybe.vpn.msg", new String[] { intf.getName() + " - " + intf.getDisplayName() });

                long res =
                        ui_manager.showMessageBox("network.admin.maybe.vpn.title", "!" + msg_details + "!", UIManagerEvent.MT_YES
                                | UIManagerEvent.MT_NO_DEFAULT);

                if (res == UIManagerEvent.MT_YES) {

                    COConfigurationManager.setParameter("User Mode", 2);
                    COConfigurationManager.setParameter("Bind IP", intf.getName());
                    COConfigurationManager.setParameter("Enforce Bind IP", true);
                    COConfigurationManager.setParameter("Check Bind IP On Start", true);
                    COConfigurationManager.save();

                    try {
                        Set<NetworkConnectionBase> connections = NetworkManager.getSingleton().getConnections();

                        Map<InetAddress, Object[]> lookup_map = new HashMap<InetAddress, Object[]>();

                        for (NetworkConnectionBase connection : connections) {

                            TransportBase tb = connection.getTransportBase();

                            if (tb instanceof Transport) {

                                boolean ok = false;

                                Transport transport = (Transport) tb;

                                if (transport.isTCP()) {

                                    TransportStartpoint start = transport.getTransportStartpoint();

                                    if (start != null) {

                                        InetSocketAddress socket_address = start.getProtocolStartpoint().getAddress();

                                        if (socket_address != null) {

                                            InetAddress address = socket_address.getAddress();

                                            Object[] details = lookup_map.get(address);

                                            if (details == null) {

                                                if (!lookup_map.containsKey(address)) {

                                                    details = getInterfaceForAddress(address);

                                                    lookup_map.put(address, details);
                                                }

                                                if (details[0] == intf) {

                                                    ok = true;
                                                }
                                            }
                                        }
                                    }
                                }

                                if (!ok) {

                                    transport.close("Explicit bind IP set, disconnecting incompatible connections");
                                }
                            }
                        }
                    } catch (Throwable e) {

                        Debug.out(e);
                    }

                    bs_last_calc = 0;

                    ui_manager.showMessageBox("settings.updated.title", "settings.updated.msg", UIManagerEvent.MT_OK);
                }
            }
        }.start();
    }

    private String getConfigKey(NetworkInterface intf) {
        try {
            return (Base32.encode(intf.getName().getBytes("UTF-8")));

        } catch (Throwable e) {

            Debug.out(e);

            return ("derp");
        }
    }

    private int categoriseIntf(NetworkInterface intf) {
        String name = intf.getName().toLowerCase();
        String desc = intf.getDisplayName().toLowerCase();

        if (desc.startsWith("tap-") || desc.contains("vpn")) {

            return (2);

        } else if (name.startsWith("ppp")) {

            return (2);

        } else if (name.startsWith("tun")) {

            return (2);

        } else if (name.startsWith("eth") || name.startsWith("en")) {

            return (1);

        } else {

            return (0);
        }
    }

    public String classifyRoute(InetAddress address) {
        synchronized (address_history) {

            if (address_history_update_time == 0) {

                return ("Initializing");
            }

            byte[] address_bytes = address.getAddress();

            AddressHistoryRecord best_entry = null;
            int best_prefix = 0;

            for (AddressHistoryRecord entry : address_history.values()) {

                InetAddress other_address = entry.getAddress();
                byte[] other_bytes = other_address.getAddress();

                if (other_bytes.length == address_bytes.length) {

                    int prefix_len = 0;

                    for (int i = 0; i < other_bytes.length; i++) {

                        byte b1 = address_bytes[i];
                        byte b2 = other_bytes[i];

                        if (b1 == b2) {

                            prefix_len += 8;

                        } else {

                            for (int j = 7; j >= 1; j--) {

                                if (((b1 >> j) & 0x01) == ((b2 >> j) & 0x01)) {

                                    prefix_len++;

                                } else {

                                    break;
                                }
                            }

                            break;
                        }
                    }

                    if (prefix_len > best_prefix) {

                        best_prefix = prefix_len;

                        best_entry = entry;
                    }
                }
            }

            if (best_entry == null) {

                return ("Unknown");
            }

            return (best_entry.getName(address_history_update_time));
        }
    }

    public Object[] getInterfaceForAddress(InetAddress address) {
        byte[] address_bytes = address.getAddress();

        Set<NetworkInterface> interfaces = old_network_interfaces;

        if (interfaces == null) {

            return (null);
        }

        NetworkInterface best_intf = null;
        InetAddress best_addr = null;
        int best_prefix = 0;

        for (NetworkInterface intf : interfaces) {

            Enumeration<InetAddress> addresses = intf.getInetAddresses();

            int num_addresses = 0;

            InetAddress derp = null;

            while (addresses.hasMoreElements()) {

                InetAddress other_address = addresses.nextElement();
                byte[] other_bytes = other_address.getAddress();

                if (other_bytes.length == address_bytes.length) {

                    num_addresses++;

                    int prefix_len = 0;

                    for (int i = 0; i < other_bytes.length; i++) {

                        byte b1 = address_bytes[i];
                        byte b2 = other_bytes[i];

                        if (b1 == b2) {

                            prefix_len += 8;

                        } else {

                            for (int j = 7; j >= 1; j--) {

                                if (((b1 >> j) & 0x01) == ((b2 >> j) & 0x01)) {

                                    prefix_len++;

                                } else {

                                    break;
                                }
                            }

                            break;
                        }
                    }

                    if (prefix_len > best_prefix) {

                        best_prefix = prefix_len;

                        best_intf = intf;

                        best_addr = null;
                        derp = other_address;
                    }
                }
            }

            if (derp != null && num_addresses > 1) {

                best_addr = derp;
            }
        }

        if (best_addr != null) {

            return (new Object[] { best_intf, best_addr });

        } else if (best_intf != null) {

            return (new Object[] { best_intf });

        } else {

            return (new Object[] { address });
        }
    }

    public void addPropertyChangeListener(NetworkAdminPropertyChangeListener listener) {
        listeners.add(listener);
    }

    public void addAndFirePropertyChangeListener(NetworkAdminPropertyChangeListener listener) {
        listeners.add(listener);

        for (int i = 0; i < NetworkAdmin.PR_NAMES.length; i++) {

            try {
                listener.propertyChanged(PR_NAMES[i]);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public void removePropertyChangeListener(NetworkAdminPropertyChangeListener listener) {
        listeners.remove(listener);
    }

    public void generate(IndentWriter writer) {
        writer.println("Network Admin");

        try {
            writer.indent();

            try {
                writer.println("Binding Details");

                writer.indent();

                boolean enforceBind = COConfigurationManager.getBooleanParameter("Enforce Bind IP");

                writer.println("bind to: " + getString(getAllBindAddresses(false)) + ", enforce=" + enforceBind);

                writer.println("bindable: " + getString(getBindableAddresses()));

                writer.println("ipv6_enabled=" + IPv6_enabled);

                writer.println("ipv4_potential=" + hasIPV4Potential());
                writer.println("ipv6_potential=" + hasIPV6Potential(false) + "/" + hasIPV6Potential(true));

                try {
                    writer.println("single homed: " + getSingleHomedServiceBindAddress());
                } catch (Throwable e) {
                    writer.println("single homed: none");
                }

                try {
                    writer.println("single homed (4): " + getSingleHomedServiceBindAddress(IP_PROTOCOL_VERSION_REQUIRE_V4));
                } catch (Throwable e) {
                    writer.println("single homed (4): none");
                }

                try {
                    writer.println("single homed (6): " + getSingleHomedServiceBindAddress(IP_PROTOCOL_VERSION_REQUIRE_V6));
                } catch (Throwable e) {
                    writer.println("single homed (6): none");
                }

                writer.println("multi homed, nio=false: " + getString(getMultiHomedServiceBindAddresses(false)));
                writer.println("multi homed, nio=true:  " + getString(getMultiHomedServiceBindAddresses(true)));

            } finally {

                writer.exdent();
            }

            NetworkAdminHTTPProxy proxy = getHTTPProxy();

            if (proxy == null) {

                writer.println("HTTP proxy: none");

            } else {

                writer.println("HTTP proxy: " + proxy.getName());

                try {

                    NetworkAdminHTTPProxy.Details details = proxy.getDetails();

                    writer.println("    name: " + details.getServerName());
                    writer.println("    resp: " + details.getResponse());
                    writer.println("    auth: " + details.getAuthenticationType());

                } catch (NetworkAdminException e) {

                    writer.println("    failed: " + e.getLocalizedMessage());
                }
            }

            NetworkAdminSocksProxy[] socks = getSocksProxies();

            if (socks.length == 0) {

                writer.println("Socks proxy: none");

            } else {

                for (int i = 0; i < socks.length; i++) {

                    NetworkAdminSocksProxy sock = socks[i];

                    writer.println("Socks proxy: " + sock.getName());

                    try {
                        String[] versions = sock.getVersionsSupported();

                        String str = "";

                        for (int j = 0; j < versions.length; j++) {

                            str += (j == 0 ? "" : ",") + versions[j];
                        }

                        writer.println("   version: " + str);

                    } catch (NetworkAdminException e) {

                        writer.println("    failed: " + e.getLocalizedMessage());
                    }
                }
            }

            try {
                NetworkAdminNATDevice[] nat_devices = getNATDevices(AzureusCoreFactory.getSingleton());

                writer.println("NAT Devices: " + nat_devices.length);

                for (int i = 0; i < nat_devices.length; i++) {

                    NetworkAdminNATDevice device = nat_devices[i];

                    writer.println("    " + device.getName() + ",address=" + device.getAddress().getHostAddress() + ":" + device.getPort()
                            + ",ext=" + device.getExternalAddress());
                }
            } catch (Exception e) {

                writer.println("Nat Devices: Can't get -> " + e.toString());
            }

            writer.println("Interfaces");

            writer.println("   " + getNetworkInterfacesAsString());

        } finally {

            writer.exdent();
        }
    }

    private String getString(InetAddress[] addresses) {
        String str = "";

        for (InetAddress address : addresses) {

            str += (str.length() == 0 ? "" : ", ") + address.getHostAddress();
        }

        return (str);
    }

    public void generateDiagnostics(final IndentWriter iw) {
        Set public_addresses = new HashSet();

        NetworkAdminHTTPProxy proxy = getHTTPProxy();

        if (proxy == null) {

            iw.println("HTTP proxy: none");

        } else {

            iw.println("HTTP proxy: " + proxy.getName());

            try {

                NetworkAdminHTTPProxy.Details details = proxy.getDetails();

                iw.println("    name: " + details.getServerName());
                iw.println("    resp: " + details.getResponse());
                iw.println("    auth: " + details.getAuthenticationType());

            } catch (NetworkAdminException e) {

                iw.println("    failed: " + e.getLocalizedMessage());
            }
        }

        NetworkAdminSocksProxy[] socks = getSocksProxies();

        if (socks.length == 0) {

            iw.println("Socks proxy: none");

        } else {

            for (int i = 0; i < socks.length; i++) {

                NetworkAdminSocksProxy sock = socks[i];

                iw.println("Socks proxy: " + sock.getName());

                try {
                    String[] versions = sock.getVersionsSupported();

                    String str = "";

                    for (int j = 0; j < versions.length; j++) {

                        str += (j == 0 ? "" : ",") + versions[j];
                    }

                    iw.println("   version: " + str);

                } catch (NetworkAdminException e) {

                    iw.println("    failed: " + e.getLocalizedMessage());
                }
            }
        }

        try {
            NetworkAdminNATDevice[] nat_devices = getNATDevices(AzureusCoreFactory.getSingleton());

            iw.println("NAT Devices: " + nat_devices.length);

            for (int i = 0; i < nat_devices.length; i++) {

                NetworkAdminNATDevice device = nat_devices[i];

                iw.println("    " + device.getName() + ",address=" + device.getAddress().getHostAddress() + ":" + device.getPort() + ",ext="
                        + device.getExternalAddress());

                public_addresses.add(device.getExternalAddress());
            }
        } catch (Exception e) {
            iw.println("Nat Devices: Can't get -> " + e.toString());
        }

        iw.println("Interfaces");

        NetworkAdminNetworkInterface[] interfaces = getInterfaces();

        if (FULL_INTF_PROBE) {

            if (interfaces.length > 0) {

                if (interfaces.length > 1 || interfaces[0].getAddresses().length > 1) {

                    for (int i = 0; i < interfaces.length; i++) {

                        networkInterface interf = (networkInterface) interfaces[i];

                        iw.indent();

                        try {

                            interf.generateDiagnostics(iw, public_addresses);

                        } finally {

                            iw.exdent();
                        }
                    }
                } else {

                    if (interfaces[0].getAddresses().length > 0) {

                        networkInterface.networkAddress address = (networkInterface.networkAddress) interfaces[0].getAddresses()[0];

                        try {
                            NetworkAdminNode[] nodes = address.getRoute(InetAddress.getByName("www.google.com"), 30000, trace_route_listener);

                            for (int i = 0; i < nodes.length; i++) {

                                networkNode node = (networkNode) nodes[i];

                                iw.println(node.getString());
                            }
                        } catch (Throwable e) {

                            iw.println("Can't resolve host for route trace - " + e.getMessage());
                        }
                    }
                }
            }
        } else {

            try {
                pingTargets(InetAddress.getByName("www.google.com"), 30000, new NetworkAdminRoutesListener() {
                    private int timeouts = 0;

                    public boolean foundNode(NetworkAdminNetworkInterfaceAddress intf, NetworkAdminNode[] route, int distance, int rtt) {
                        iw.println(intf.getAddress().getHostAddress() + ": " + route[route.length - 1].getAddress().getHostAddress() + " ("
                                + distance + ")");

                        return (false);
                    }

                    public boolean timeout(NetworkAdminNetworkInterfaceAddress intf, NetworkAdminNode[] route, int distance) {
                        iw.println(intf.getAddress().getHostAddress() + ": timeout (dist=" + distance + ")");

                        timeouts++;

                        return (timeouts < 3);
                    }
                });

            } catch (Throwable e) {

                iw.println("getRoutes failed: " + Debug.getNestedExceptionMessage(e));
            }
        }

        iw.println("Inbound protocols: default routing");

        if (AzureusCoreFactory.isCoreRunning()) {
            AzureusCore azureus_core = AzureusCoreFactory.getSingleton();

            NetworkAdminProtocol[] protocols = getInboundProtocols(azureus_core);

            for (int i = 0; i < protocols.length; i++) {

                NetworkAdminProtocol protocol = protocols[i];

                try {
                    InetAddress ext_addr = testProtocol(protocol);

                    if (ext_addr != null) {

                        public_addresses.add(ext_addr);
                    }

                    iw.println("    " + protocol.getName() + " - " + ext_addr);

                } catch (NetworkAdminException e) {

                    iw.println("    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
                }
            }

            iw.println("Outbound protocols: default routing");

            protocols = getOutboundProtocols(azureus_core);

            for (int i = 0; i < protocols.length; i++) {

                NetworkAdminProtocol protocol = protocols[i];

                try {

                    InetAddress ext_addr = testProtocol(protocol);

                    if (ext_addr != null) {

                        public_addresses.add(ext_addr);
                    }

                    iw.println("    " + protocol.getName() + " - " + ext_addr);

                } catch (NetworkAdminException e) {

                    iw.println("    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
                }
            }
        }

        Iterator it = public_addresses.iterator();

        iw.println("Public Addresses");

        while (it.hasNext()) {

            InetAddress pub_address = (InetAddress) it.next();

            try {
                NetworkAdminASN res = lookupCurrentASN(pub_address);

                iw.println("    " + pub_address.getHostAddress() + " -> " + res.getAS() + "/" + res.getASName());

            } catch (Throwable e) {

                iw.println("    " + pub_address.getHostAddress() + " -> " + Debug.getNestedExceptionMessage(e));
            }
        }
    }

    protected class networkInterface implements NetworkAdminNetworkInterface {
        private NetworkInterface ni;

        protected networkInterface(NetworkInterface _ni) {
            ni = _ni;
        }

        public String getDisplayName() {
            return (ni.getDisplayName());
        }

        public String getName() {
            return (ni.getName());
        }

        public NetworkAdminNetworkInterfaceAddress[] getAddresses() {
            // BAH NetworkInterface has lots of goodies but is 1.6

            Enumeration e = ni.getInetAddresses();

            List addresses = new ArrayList();

            while (e.hasMoreElements()) {

                InetAddress address = (InetAddress) e.nextElement();

                if ((address instanceof Inet6Address) && !IPv6_enabled) {

                    continue;
                }

                addresses.add(new networkAddress(address));
            }

            return ((NetworkAdminNetworkInterfaceAddress[]) addresses.toArray(new NetworkAdminNetworkInterfaceAddress[addresses.size()]));
        }

        public String getString() {
            String str = getDisplayName() + "/" + getName() + " [";

            NetworkAdminNetworkInterfaceAddress[] addresses = getAddresses();

            for (int i = 0; i < addresses.length; i++) {

                networkAddress addr = (networkAddress) addresses[i];

                str += (i == 0 ? "" : ",") + addr.getAddress().getHostAddress();
            }

            return (str + "]");
        }

        public void generateDiagnostics(IndentWriter iw, Set public_addresses) {
            iw.println(getDisplayName() + "/" + getName());

            NetworkAdminNetworkInterfaceAddress[] addresses = getAddresses();

            for (int i = 0; i < addresses.length; i++) {

                networkAddress addr = (networkAddress) addresses[i];

                iw.indent();

                try {

                    addr.generateDiagnostics(iw, public_addresses);

                } finally {

                    iw.exdent();
                }
            }
        }

        protected class networkAddress implements NetworkAdminNetworkInterfaceAddress {
            private InetAddress address;

            protected networkAddress(

            InetAddress _address) {
                address = _address;
            }

            public NetworkAdminNetworkInterface getInterface() {
                return (networkInterface.this);
            }

            public InetAddress getAddress() {
                return (address);
            }

            public boolean isLoopback() {
                return (address.isLoopbackAddress());
            }

            public NetworkAdminNode[] getRoute(InetAddress target, final int max_millis, final NetworkAdminRouteListener listener)

            throws NetworkAdminException {
                return (NetworkAdminImpl.this.getRoute(address, target, max_millis, listener));
            }

            public NetworkAdminNode pingTarget(InetAddress target, final int max_millis, final NetworkAdminRouteListener listener)

            throws NetworkAdminException {
                return (NetworkAdminImpl.this.pingTarget(address, target, max_millis, listener));
            }

            public InetAddress testProtocol(NetworkAdminProtocol protocol)

            throws NetworkAdminException {
                return (protocol.test(this));
            }

            public void generateDiagnostics(IndentWriter iw, Set public_addresses) {
                iw.println("" + getAddress());

                try {
                    iw.println("  Trace route");

                    iw.indent();

                    if (isLoopback()) {

                        iw.println("Loopback - ignoring");

                    } else {

                        try {
                            NetworkAdminNode[] nodes = getRoute(InetAddress.getByName("www.google.com"), 30000, trace_route_listener);

                            for (int i = 0; i < nodes.length; i++) {

                                networkNode node = (networkNode) nodes[i];

                                iw.println(node.getString());
                            }
                        } catch (Throwable e) {

                            iw.println("Can't resolve host for route trace - " + e.getMessage());
                        }

                        iw.println("Outbound protocols: bound");

                        AzureusCore azureus_core = AzureusCoreFactory.getSingleton();

                        NetworkAdminProtocol[] protocols = getOutboundProtocols(azureus_core);

                        for (int i = 0; i < protocols.length; i++) {

                            NetworkAdminProtocol protocol = protocols[i];

                            try {
                                InetAddress res = testProtocol(protocol);

                                if (res != null) {

                                    public_addresses.add(res);
                                }

                                iw.println("    " + protocol.getName() + " - " + res);

                            } catch (NetworkAdminException e) {

                                iw.println("    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
                            }
                        }

                        iw.println("Inbound protocols: bound");

                        protocols = getInboundProtocols(azureus_core);

                        for (int i = 0; i < protocols.length; i++) {

                            NetworkAdminProtocol protocol = protocols[i];

                            try {
                                InetAddress res = testProtocol(protocol);

                                if (res != null) {

                                    public_addresses.add(res);
                                }

                                iw.println("    " + protocol.getName() + " - " + res);

                            } catch (NetworkAdminException e) {

                                iw.println("    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
                            }
                        }
                    }
                } finally {

                    iw.exdent();
                }
            }
        }
    }

    protected class networkNode implements NetworkAdminNode {
        private InetAddress address;
        private int distance;
        private int rtt;

        protected networkNode(InetAddress _address, int _distance, int _millis) {
            address = _address;
            distance = _distance;
            rtt = _millis;
        }

        public InetAddress getAddress() {
            return (address);
        }

        public boolean isLocalAddress() {
            return (address.isLinkLocalAddress() || address.isSiteLocalAddress());
        }

        public int getDistance() {
            return (distance);
        }

        public int getRTT() {
            return (rtt);
        }

        protected String getString() {
            if (address == null) {

                return ("" + distance);

            } else {

                return (distance + "," + address + "[local=" + isLocalAddress() + "]," + rtt);
            }
        }
    }

    protected void generateDiagnostics(IndentWriter iw, NetworkAdminProtocol[] protocols) {
        for (int i = 0; i < protocols.length; i++) {

            NetworkAdminProtocol protocol = protocols[i];

            iw.println("Testing " + protocol.getName());

            try {
                InetAddress ext_addr = testProtocol(protocol);

                iw.println("    -> OK, public address=" + ext_addr);

            } catch (NetworkAdminException e) {

                iw.println("    -> Failed: " + Debug.getNestedExceptionMessage(e));
            }
        }
    }

    public void logNATStatus(IndentWriter iw) {
        if (AzureusCoreFactory.isCoreRunning()) {
            generateDiagnostics(iw, getInboundProtocols(AzureusCoreFactory.getSingleton()));
        }
    }

    private static class AddressHistoryRecord {
        private String ni_name;
        private boolean ni_has_multiple_addresses;
        private InetAddress address;
        private long last_seen;

        private AddressHistoryRecord(NetworkInterface _ni, InetAddress _a, long _now) {
            ni_name = _ni.getName();
            address = _a;
            last_seen = _now;

            Enumeration<InetAddress> addresses = _ni.getInetAddresses();

            int hits = 0;

            int len = address.getAddress().length;

            while (addresses.hasMoreElements()) {

                if (addresses.nextElement().getAddress().length == len) {

                    hits++;
                }
            }

            ni_has_multiple_addresses = hits > 1;
        }

        private void setLastSeen(long t) {
            last_seen = t;
        }

        private long getLastSeen() {
            return (last_seen);
        }

        private InetAddress getAddress() {
            return (address);
        }

        private String getName(long last_update) {
            String result = ni_name;

            if (ni_has_multiple_addresses) {

                result += "/" + address.getHostAddress();
            }

            if (last_update > last_seen) {

                result += " (disconnected)";
            }

            return (result);
        }
    }

    public static void main(String[] args) {
        boolean TEST_SOCKS_PROXY = false;
        boolean TEST_HTTP_PROXY = false;

        try {
            if (TEST_SOCKS_PROXY) {

                AESocksProxy proxy = AESocksProxyFactory.create(4567, 10000, 10000);

                proxy.setAllowExternalConnections(true);

                System.setProperty("socksProxyHost", "localhost");
                System.setProperty("socksProxyPort", "4567");
            }

            if (TEST_HTTP_PROXY) {

                System.setProperty("http.proxyHost", "localhost");
                System.setProperty("http.proxyPort", "3128");
                System.setProperty("https.proxyHost", "localhost");
                System.setProperty("https.proxyPort", "3128");

                Authenticator.setDefault(new Authenticator() {
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return (new PasswordAuthentication("fred", "bill".toCharArray()));
                    }
                });

            }

            IndentWriter iw = new IndentWriter(new PrintWriter(System.out));

            iw.setForce(true);

            COConfigurationManager.initialise();

            AzureusCoreFactory.create();

            NetworkAdmin admin = getSingleton();

            // admin.logNATStatus( iw );
            admin.generateDiagnostics(iw);

        } catch (Throwable e) {

            e.printStackTrace();
        }
    }
}
